﻿using System;
using System.Threading.Tasks;

namespace Bettzueche.RLLibrary.Types
{
    /// <summary>
    /// Rudimentäre (m x n) Matrix (double-Werte).<para>
    /// Mit Addition zweier Matrizen und Multiplikation mit einem Skalar</para>
    /// </summary>
    public class Matrix
    {
        int m; // number rows
        int n; // number columns
        double[][] data; // the values
        Matrix _transposed;


        #region Indexer, Properties

        /// <summary>
        /// Gets or sets the <see cref="System.Double"/>-value for the specified (row, column) index.
        /// </summary>
        /// <value>
        /// The <see cref="System.Double"/> value.
        /// </value>
        /// <param name="rowIdx">Index of the row.</param>
        /// <param name="colIdx">Index of the column.</param>
        /// <returns></returns>
        public double this[int rowIdx, int colIdx] {
            get { return data[rowIdx][colIdx]; }
            set { data[rowIdx][colIdx] = value; }
        }

        /// <summary>
        /// Gets the rows count.
        /// </summary>
        /// <value>
        /// The rows.
        /// </value>
        public int Rows { get { return m; } }

        /// <summary>
        /// Gets the columns count.
        /// </summary>
        /// <value>
        /// The columns.
        /// </value>
        public int Columns { get { return n; } }

        /// <summary>
        /// Gets the transposed matrix
        /// </summary>
        /// <value>
        /// The transposed matrix
        /// </value>
        /// <remarks>
        /// At first call a singleton transposed matrix will be created and kept for future calls.
        /// </remarks>
        public Matrix Transposed {
            get {
                if (_transposed == null) {
                    _transposed = new Matrix();
                    _transposed.m = this.n;
                    _transposed.n = this.m;
                    _transposed.data = Matrix.MatrixTranspose(this.data);
                }
                return _transposed;
            }
        }

        #endregion

        #region Constructor(s)

        /// <summary>
        /// Initializes a new instance of the <see cref="Matrix"/> class.
        /// </summary>
        /// <param name="rows">The rows.</param>
        /// <param name="columns">The columns.</param>
        /// <exception cref="System.ArgumentException">
        /// rows have to be at least one;rows
        /// or
        /// columns have to be at least one;columns
        /// </exception>
        public Matrix(int rows, int columns) {
            if (rows < 1) {
                throw new ArgumentException("rows have to be at least one", "rows");
            }
            if (columns < 1) {
                throw new ArgumentException("columns have to be at least one", "columns");
            }
            data = Matrix.MatrixCreate(rows, columns);
            this.m = rows;
            this.n = columns;
        }

        private Matrix() { 
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Matrix"/> class with a copy of the
        /// specified values.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="System.ArgumentException">Non-conformable parameter</exception>
        public Matrix(double[][] matrix) {
            if (matrix == null)
                throw new ArgumentNullException();
            m = matrix.Length;
            data = new double[m][];
            if (m == 0)
                return;
            n = matrix[0].Length;
            for (int r = 0; r < m; r++) {
                if (matrix[r].Length != n)
                    throw new ArgumentException("Non-conformable parameter");
                Array.Copy(matrix[r], data[r], n);
            }
        }

        #endregion

        #region public

        /// <summary>
        /// Adds the specified matrix b to this matrix.
        /// </summary>
        /// <param name="matrixB">The matrix b.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="System.ArgumentException">Non-conformable matrices</exception>
        public void Add(Matrix matrixB) {
            if (matrixB == null)
                throw new ArgumentNullException();
            if (this.m != matrixB.m || this.n != matrixB.n)
                throw new ArgumentException("Non-conformable matrices");
            Parallel.For(0, this.m, r => {
                for (int c = 0; c < this.n; c++) {
                    data[r][c] += matrixB.data[r][c];
                }
            });
        }

        /// <summary>
        /// Clears this instance.
        /// Setzt alle Werte auf 0.
        /// </summary>
        public void Clear() {
            Parallel.For(0, this.m, r => {
                data[r] = new double[this.n];
            });
        }

        #endregion

        #region static (and private)

        static double[][] MatrixTranspose(double[][] matrix) {
            int rows = matrix.Length;
            int cols = matrix[0].Length;
            double[][] result = MatrixCreate(cols, rows);
            Parallel.For(0, rows, r => {
                for (int c = 0; c < cols; c++) {
                    result[c][r] = matrix[r][c];
                }
            });
            return result;
        }

        static double[][] MatrixCreate(int rows, int cols) {
            // do error checking here?
            double[][] result = new double[rows][];
            for (int r = 0; r < rows; r++)
                result[r] = new double[cols]; // auto init to 0.0
            return result;
        }

        static double[][] MatrixProduct(double[][] matrixA, double[][] matrixB) {
            int aRows = matrixA.Length;
            int aCols = matrixA[0].Length;
            int bCols = matrixB[0].Length;
            //if (aCols != bRows)
            //    throw new Exception("Non-conformable matrices in MatrixProduct");
            double[][] result = MatrixCreate(aRows, bCols);
            Parallel.For(0, aRows, i => {
                for (int j = 0; j < bCols; ++j)
                    for (int k = 0; k < aCols; ++k)
                        result[i][j] += matrixA[i][k] * matrixB[k][j];
            });

            return result;
        }

        static double[][] MatrixSum(double[][] matrixA, double[][] matrixB) {
            int aRows = matrixA.Length;
            int aCols = matrixA[0].Length;
            //if (aCols != bCols || aRows != bRows)
            //    throw new Exception("Non-conformable matrices in MatrixProduct");
            double[][] result = MatrixCreate(aRows, aCols);
            Parallel.For(0, aRows, r => {
                for (int c = 0; c < aCols; c++) {
                    result[r][c] = matrixA[r][c] + matrixB[r][c];
                }
            });
            return result;
        }

        static double[][] MatrixScalarProduct(double[][] matrixA, double scalar) {
            int aRows = matrixA.Length;
            int aCols = matrixA[0].Length;
            //if (aCols != bRows)
            //    throw new Exception("Non-conformable matrices in MatrixProduct");
            double[][] result = MatrixCreate(aRows, aCols);
            Parallel.For(0, aRows, r => {
                for (int c = 0; c < aCols; ++c)
                    result[r][c] = matrixA[r][c] * scalar;
            });
            return result;
        }

        #endregion

        #region Operators

        /// <summary>
        /// Implements the operator +.
        /// </summary>
        /// <param name="A">Matrix A.</param>
        /// <param name="B">Matrix B.</param>
        /// <returns>
        /// The result of the operator.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">
        /// A
        /// or
        /// B
        /// </exception>
        /// <exception cref="System.ArgumentException">Non-conformable matrices</exception>
        public static Matrix operator +(Matrix A, Matrix B) {
            if (null == A)
                throw new ArgumentNullException("A");
            if (null == B)
                throw new ArgumentNullException("B");
            if (A.m != B.m || A.n != B.n)
                throw new ArgumentException("Non-conformable matrices");
            Matrix retVal = new Matrix();
            retVal.m = A.m;
            retVal.n = B.n;
            retVal.data = Matrix.MatrixSum(A.data, B.data);
            return retVal;
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="A">Matrix A.</param>
        /// <param name="B">Matrix B.</param>
        /// <returns>
        /// The result of the operator.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">
        /// A
        /// or
        /// B
        /// </exception>
        /// <exception cref="System.ArgumentException">Non-conformable matrices</exception>
        public static Matrix operator *(Matrix A, Matrix B) {
            if (null == A)
                throw new ArgumentNullException("A");
            if (null == B)
                throw new ArgumentNullException("B");
            if (A.n != B.m)
                throw new ArgumentException("Non-conformable matrices");
            Matrix retVal = new Matrix();
            retVal.m = A.m;
            retVal.n = A.n;
            retVal.data = Matrix.MatrixProduct(A.data, B.data);
            return retVal;
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="A">Matrix A.</param>
        /// <param name="s">scalar s.</param>
        /// <returns>
        /// The result of the operator.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">A</exception>
        public static Matrix operator *(Matrix A, double s) {
            if (null == A)
                throw new ArgumentNullException("A");
            Matrix retVal = new Matrix();
            retVal.m = A.m;
            retVal.n = A.n;
            retVal.data = Matrix.MatrixScalarProduct(A.data, s);
            return retVal;
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="s">scalar s.</param>
        /// <param name="A">Matrix A.</param>
        /// <returns>
        /// The result of the operator.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">A</exception>
        public static Matrix operator *(double s, Matrix A) {
            return A * s;
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="s">scalar s.</param>
        /// <param name="A">Matrix A.</param>
        /// <returns>
        /// The result of the operator.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">A</exception>
        public static Matrix operator /(double s, Matrix A) {
            return A * (1.0/s);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="A">Matrix A.</param>
        /// <param name="s">scalar s.</param>
        /// <returns>
        /// The result of the operator.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">A</exception>
        public static Matrix operator /(Matrix A, double s) {
            return A * (1.0/s);
        }

        #endregion
    }
}
